package dswork.html.parser;

import java.util.Arrays;

import dswork.html.nodes.Entities;

/**
 * Readers the input stream into tokens.
 */
final class Tokeniser
{
	static final char replacementChar = '\uFFFD'; // replaces null character
	private static final char[] notCharRefCharsSorted = new char[]
	{
			'\t', '\n', '\r', '\f', ' ', '<', '&'
	};
	static
	{
		Arrays.sort(notCharRefCharsSorted);
	}
	private final HtmlReader reader; // html input
	private TokeniserState state = TokeniserState.Data; // current tokenisation state
	private Token emitPending; // the token we are about to emit on next read
	private boolean isEmitPending = false;
	private String charsString = null; // characters pending an emit. Will fall to charsBuilder if more than one
	private StringBuilder charsBuilder = new StringBuilder(1024); // buffers characters to output as one token, if more
																	// than one emit per read
	StringBuilder dataBuffer = new StringBuilder(1024); // buffers data looking for </script>
	Token.Tag tagPending; // tag we are building up
	Token.StartTag startPending = new Token.StartTag();
	Token.EndTag endPending = new Token.EndTag();
	Token.Character charPending = new Token.Character();
	Token.Doctype doctypePending = new Token.Doctype(); // doctype building up
	Token.Comment commentPending = new Token.Comment(); // comment building up
	private String lastStartTag; // the last start tag emitted, to test appropriate end tag
	private boolean selfClosingFlagAcknowledged = true;

	Tokeniser(HtmlReader reader)
	{
		this.reader = reader;
	}

	Token read()
	{
		if(!selfClosingFlagAcknowledged)
		{
			selfClosingFlagAcknowledged = true;
		}
		while(!isEmitPending)
			state.read(this, reader);
		// if emit is pending, a non-character token was found: return any chars in buffer, and leave token for next
		// read:
		if(charsBuilder.length() > 0)
		{
			String str = charsBuilder.toString();
			charsBuilder.delete(0, charsBuilder.length());
			charsString = null;
			return charPending.data(str);
		}
		else if(charsString != null)
		{
			Token token = charPending.data(charsString);
			charsString = null;
			return token;
		}
		else
		{
			isEmitPending = false;
			return emitPending;
		}
	}

	void emit(Token token)
	{
		emitPending = token;
		isEmitPending = true;
		if(token.type == Token.TokenType.StartTag)
		{
			Token.StartTag startTag = (Token.StartTag) token;
			lastStartTag = startTag.tagName;
			if(startTag.selfClosing)
				selfClosingFlagAcknowledged = false;
		}
	}

	void emit(final String str)
	{
		if(charsString == null)
		{
			charsString = str;
		}
		else
		{
			if(charsBuilder.length() == 0)
			{
				charsBuilder.append(charsString);
			}
			charsBuilder.append(str);
		}
	}

	void emit(char[] chars)
	{
		emit(String.valueOf(chars));
	}

	void emit(int[] codepoints)
	{
		emit(new String(codepoints, 0, codepoints.length));
	}

	void emit(char c)
	{
		emit(String.valueOf(c));
	}

	TokeniserState getState()
	{
		return state;
	}

	void transition(TokeniserState state)
	{
		this.state = state;
	}

	void advanceTransition(TokeniserState state)
	{
		reader.advance();
		this.state = state;
	}

	void acknowledgeSelfClosingFlag()
	{
		selfClosingFlagAcknowledged = true;
	}
	final private int[] codepointHolder = new int[1]; // holder to not have to keep creating arrays
	final private int[] multipointHolder = new int[2];

	int[] consumeCharacterReference(Character additionalAllowedCharacter, boolean inAttribute)
	{
		if(reader.isEmpty())
			return null;
		if(additionalAllowedCharacter != null && additionalAllowedCharacter == reader.current())
			return null;
		if(reader.matchesAnySorted(notCharRefCharsSorted))
			return null;
		final int[] codeRef = codepointHolder;
		reader.mark();
		if(reader.matchConsume("#"))
		{ // numbered
			boolean isHexMode = reader.matchConsumeIgnoreCase("X");
			String numRef = isHexMode ? reader.consumeHexSequence() : reader.consumeDigitSequence();
			if(numRef.length() == 0)
			{ // didn't match anything
				// characterReferenceError("numeric reference with no numerals");
				reader.rewindToMark();
				return null;
			}
			if(!reader.matchConsume(";"))
				;
			// characterReferenceError("missing semicolon"); // missing semi
			int charval = -1;
			try
			{
				int base = isHexMode ? 16 : 10;
				charval = Integer.valueOf(numRef, base);
			}
			catch(NumberFormatException e)
			{
			} // skip
			if(charval == -1 || (charval >= 0xD800 && charval <= 0xDFFF) || charval > 0x10FFFF)
			{
				// characterReferenceError("character outside of valid range");
				codeRef[0] = replacementChar;
				return codeRef;
			}
			else
			{
				// todo: implement number replacement table
				// todo: check for extra illegal unicode points as parse errors
				codeRef[0] = charval;
				return codeRef;
			}
		}
		else
		{ // named
			// get as many letters as possible, and look for matching entities.
			String nameRef = reader.consumeLetterThenDigitSequence();
			boolean looksLegit = reader.matches(';');
			// found if a base named entity without a ;, or an extended entity with the ;.
			boolean found = (Entities.isNamedEntity(nameRef) && looksLegit);
			if(!found)
			{
				reader.rewindToMark();
				// if (looksLegit) // named with semicolon
				// characterReferenceError(String.format("invalid named referenece '%s'", nameRef));
				return null;
			}
			if(inAttribute && (reader.matchesLetter() || reader.matchesDigit() || reader.matchesAny('=', '-', '_')))
			{
				// don't want that to match
				reader.rewindToMark();
				return null;
			}
			if(!reader.matchConsume(";"))
				;
			// characterReferenceError("missing semicolon"); // missing semi
			int numChars = Entities.codepointsForName(nameRef, multipointHolder);
			if(numChars == 1)
			{
				codeRef[0] = multipointHolder[0];
				return codeRef;
			}
			else if(numChars == 2)
			{
				return multipointHolder;
			}
			else
			{
				return multipointHolder;
			}
		}
	}

	Token.Tag createTagPending(boolean start)
	{
		tagPending = start ? startPending.reset() : endPending.reset();
		return tagPending;
	}

	void emitTagPending()
	{
		tagPending.finaliseTag();
		emit(tagPending);
	}

	void createCommentPending()
	{
		commentPending.reset();
	}

	void emitCommentPending()
	{
		emit(commentPending);
	}

	void createDoctypePending()
	{
		doctypePending.reset();
	}

	void emitDoctypePending()
	{
		emit(doctypePending);
	}

	void createTempBuffer()
	{
		Token.reset(dataBuffer);
	}

	boolean isAppropriateEndTagToken()
	{
		return lastStartTag != null && tagPending.name().equalsIgnoreCase(lastStartTag);
	}

	String appropriateEndTagName()
	{
		if(lastStartTag == null)
			return null;
		return lastStartTag;
	}

	boolean currentNodeInHtmlNS()
	{
		// todo: implement namespaces correctly
		return true;
		// Element currentNode = currentNode();
		// return currentNode != null && currentNode.namespace().equals("HTML");
	}

	/**
	 * Utility method to consume reader and unescape entities found within.
	 * @param inAttribute
	 * @return unescaped string from reader
	 */
	String unescapeEntities(boolean inAttribute)
	{
		StringBuilder builder = new StringBuilder();
		while(!reader.isEmpty())
		{
			builder.append(reader.consumeTo('&'));
			if(reader.matches('&'))
			{
				reader.consume();
				int[] c = consumeCharacterReference(null, inAttribute);
				if(c == null || c.length == 0)
					builder.append('&');
				else
				{
					builder.appendCodePoint(c[0]);
					if(c.length == 2)
						builder.appendCodePoint(c[1]);
				}
			}
		}
		return builder.toString();
	}
}
